Repository navigation
fix(transaction): in BEGIN, take the write gate before the public mutex - #1516
Conversation
A checkpoint closes the write gate, then waits up to the checkpoint wait timeout for open write transactions to leave. A write BEGIN arriving meanwhile took the public mutex and then blocked on the gate while holding it. Every BEGIN, COMMIT and ROLLBACK needs that mutex, so the open writers could not leave, reads could not start, and the checkpoint timed out. Its next attempt, on the next commit past the threshold, met the same state, so under steady load with three or more writers the database stalled for the timeout over and over and no checkpoint completed. A write BEGIN now takes the gate first and the public mutex second. A BEGIN waiting for a checkpoint holds nothing the open writers or readers need. The wait for a pending forced checkpoint at the top of beginTransaction existed for this same reason and is now part of the mustWait loop. A BEGIN waiting at the gate also made the checkpoint's try_lock on the public mutex fail, which pinned the WAL with nothing open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checkpoint sampled the open transactions after closing the write gate with a try_lock, because a write BEGIN could hold the public mutex while blocked on the gate. When the try_lock failed it fell back to a horizon captured before the gate, or to its own snapshot timestamp if nothing had been open. Either way the checkpoint counted as pinned and kept the WAL, even with no transaction open, so the WAL grew past the threshold and every later commit ran another checkpoint. No holder of the public mutex blocks on the gate any more, so the checkpoint now takes the mutex and always samples what is really open. The capture before the gate and both fallbacks are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note on what the two mutexes protect (for future readers):
Two mutexes exist so the drain can wait without blocking the very writers it waits for. The ordering invariant after this PR is gate → public: nothing may block on the gate while holding public, which is what lets |
This work was produced with the help of language models.
Fixes #1514.
With a write transaction open on B, A's commit starting an automatic checkpoint that waits for B, and a write from C arriving meanwhile, B's
COMMITtook 3.6 s instead of 4 ms and a read on a fourth connection took 4.1 s instead of 6 ms. The checkpoint then failed. C'sBEGINheld the mutex everyBEGIN,COMMITandROLLBACKtakes while it blocked on the write gate, so B could not leave. Under steady load with three or more writers this repeated until the load stopped. Multi-writer mode is the default since #1435.Fix. A write
BEGINnow takes the write gate first and the public mutex second, so one waiting for a checkpoint holds nothing that open writers or readers need. #1507 solved this for forced checkpoints; its separate wait is now part of themustWaitloop.The checkpoint now samples the open transactions with a plain lock, since no holder of the public mutex waits on the gate. A failed
try_lockthere kept the WAL even with nothing open, and every later commit ran another checkpoint. The capture before the gate and both fallbacks are gone.main→ this PRTwo 40-60 s runs per row through the C API, with a reader at 50/s and the default 16 MB threshold unless noted. Four writers at 50 ms with
auto_checkpoint=falsereach 61-68 commits/s onmain, the ceiling for that load.Cost. Instructions per operation stay within 0.04% of
mainfor a point read and forBEGIN,CREATEandCOMMIT, single-threaded. No class layout changes.Not fixed here. While a long write transaction stays open, a commit that starts the automatic checkpoint still waits out the 5 s timeout; #1515 tracks it.
Tests.
WriterWaitingBehindAutoCheckpointLetsOthersThroughhas a writer arrive while a checkpoint waits for an open one, then expects a read and the open writer'sCOMMITto finish within 5 s against a 10 s checkpoint timeout; onmainthey take 11 s.WriterWaitingAtWriteGateDoesNotPinCheckpointhas aBEGINarrive as the checkpoint starts and expects an empty WAL;mainleaves 216 bytes.BusyPublicMutexDoesNotPinCheckpointholds the public mutex for 100 ms at the same point and expects the same; it fails with only theBEGINchange. Full ctest: 3360/3360.